<?php

	namespace org\tekuna\framework\context;


	use \Exception;
	use \ReflectionClass;

	use org\tekuna\core\configuration\ConfigurationElement;
	use org\tekuna\core\configuration\ConfigurationException;
	use org\tekuna\core\context\Context;

	/**
	 * This class puts new objects or replaces such in a given context. The
	 * child nodes 'context' of a given ConfigurationElement are evaluated
	 * for this task.
	 *
	 * <framework:context name="Foo" class="foo\Bar" />
	 *
	 * This example (formatted as XML configuration) puts a new instance of the
	 * class foo\Bar in the Context and uses for reference the name 'foo'. The
	 * following types of context 'objects' are supported:
	 *
	 * class, object     ... a new instance of this object
	 * value, string     ... a string
	 * int, integer      ... an integer
	 * float, double     ... a float
	 * bool, boolean     ... a boolean (values 'true' or 'false')
	 * array             ... an array of strings; elements are delimited by comma (,)
	 *
	 * <framework:context name="Foo" array="1,2,3" />
	 *
	 * This example puts a new object named 'foo' which is an array of strings
	 * that holds the values '1', '2' and '3'.
	 *
	 * To overwrite existing context objects you must define the override parameter:
	 *
	 * <framework:context name="Existing" value="new value" override="true" />
	 *
	 * If you attempt to override an existing object without this parameter, an
	 * ConfigurationException is thrown.
	 * 
	 * If the process of building the new object for the context is more complex or 
	 * different types of objects are generated, use the parameter builder.
	 * 
	 * <framework:context name="BuiltObject" builder="my/packages/ObjectBuilder" />
	 * 
	 * Classes referenced by the builder parameter need to implement the ContextObjectBuilder
	 * interface with one method. This method finally returns the value that is put
	 * into the context.
	 */
	class ContextProcessor {

		/** the interface an object builder must implement */
		const CONTEXT_OBJECT_BUILDER_INTERFACE = 'org\tekuna\framework\context\ContextObjectBuilder';

		private $objContext;

		/**
		 * Construct a new ContextUpdater and provide the Context to work on.
		 *
		 * @param Context $objContext the context to work on
		 */
		public function __construct(Context $objContext) {

			$this -> objContext = $objContext;
		}


		/**
		 * Perform the Context update by using the given parent element.
		 *
		 * @param ConfigurationElement $objParentElement the element whose child elements are searched for 'context' elements with aspect 'framework'
		 * @throws ConfigurationException if the object type is not supported or tried to override existing value without override parameter
		 */
		public function updateContext(ConfigurationElement $objParentElement) {

			// find all context nodes
			$arrContextElements = $objParentElement -> getAllChildElements('framework', 'context');

			// iterate those elements
			foreach ($arrContextElements as $objElement) {

				try {

					// ensure that there is the name attribute
					if ($objElement -> hasAttribute('framework', 'name')) {
						$this -> handleContextElement($objElement);
					}
					else {

						throw new ConfigurationException("The 'context' element must have a name attribute: ". trim($objElement -> __toString()));
					}
				}
				catch (Exception $objException) {

					throw new ConfigurationException("Error while updating the context with element ". trim($objElement -> __toString()), -1, $objException);
				}
			}
			
			// autowire all context object with each other
			$this -> objContext -> autowireContextObjects();
		}


		private function handleContextElement(ConfigurationElement $objElement) {

			// get the name for the element in the context
			$sContextName = $objElement -> getAttribute('framework', 'name') -> getValue();

			// check if an object with this name is already in the context
			if ($this -> objContext -> hasObject($sContextName)) {

				$bOverride = false;
				if ($objElement -> hasAttribute('framework', 'override')) {

					$bOverride = $objElement -> getAttribute('framework', 'override') -> getValueAsBoolean();
				}

				// throw exception if override parameter not given or not set to true
				if (!$bOverride) {

					throw new ConfigurationException("There is already an object with name '$sContextName' in the context. Specify the override parameter to 'true' if you want to override this object in the context.");
				}
			}

			if ($objElement -> hasAttribute('framework', 'value')) {

				$sContextValue = $objElement -> getAttribute('framework', 'value') -> getValue();
				$this -> objContext -> setObject($sContextName, $sContextValue);
				return;
			}

			if ($objElement -> hasAttribute('framework', 'string')) {

				$sContextValue = $objElement -> getAttribute('framework', 'string') -> getValue();
				$this -> objContext -> setObject($sContextName, $sContextValue);
				return;
			}

			if ($objElement -> hasAttribute('framework', 'object')) {

				$sClassName = trim($objElement -> getAttribute('framework', 'object') -> getValue());
				$this -> objContext -> setObject($sContextName, new $sClassName());
				return;
			}

			if ($objElement -> hasAttribute('framework', 'class')) {

				$sClassName = trim($objElement -> getAttribute('framework', 'class') -> getValue());
				$this -> objContext -> setObject($sContextName, new $sClassName());
				return;
			}

			if ($objElement -> hasAttribute('framework', 'builder')) {

				// get the builder class
				$sBuilderClassName = $objElement -> getAttribute('framework', 'builder') -> getValue();

				// check if the class implements the ContextObjectBuilder interface
				$objRC = new ReflectionClass($sBuilderClassName);
				if (! in_array(self :: CONTEXT_OBJECT_BUILDER_INTERFACE, $objRC -> getInterfaceNames())) {

					throw new ConfigurationException("The given builder class '$sBuilderClassName' does not implement the necessary interface '". self :: CONTEXT_OBJECT_BUILDER_INTERFACE ."'.");
				}

				// build the context object
				$objBuiltContextObject = $sBuilderClassName :: buildContextObject($this -> objContext, $objElement);

				$this -> objContext -> setObject($sContextName, $objBuiltContextObject);
				return;
			}

			if ($objElement -> hasAttribute('framework', 'int')) {

				$iContextValue = $objElement -> getAttribute('framework', 'int') -> getValueAsInteger();
				$this -> objContext -> setObject($sContextName, $iContextValue);
				return;
			}

			if ($objElement -> hasAttribute('framework', 'integer')) {

				$iContextValue = $objElement -> getAttribute('framework', 'integer') -> getValueAsInteger();
				$this -> objContext -> setObject($sContextName, $iContextValue);
				return;
			}

			if ($objElement -> hasAttribute('framework', 'float')) {

				$fContextValue = $objElement -> getAttribute('framework', 'float') -> getValueAsFloat();
				$this -> objContext -> setObject($sContextName, $fContextValue);
				return;
			}

			if ($objElement -> hasAttribute('framework', 'double')) {

				$fContextValue = (double) $objElement -> getAttribute('framework', 'double') -> getValueAsFloat();
				$this -> objContext -> setObject($sContextName, $fContextValue);
				return;
			}

			if ($objElement -> hasAttribute('framework', 'bool')) {

				$bContextValue = $objElement -> getAttribute('framework', 'bool') -> getValueAsBoolean();
				$this -> objContext -> setObject($sContextName, $bContextValue);
				return;
			}

			if ($objElement -> hasAttribute('framework', 'boolean')) {

				$bContextValue = $objElement -> getAttribute('framework', 'boolean') -> getValueAsBoolean();
				$this -> objContext -> setObject($sContextName, $bContextValue);
				return;
			}

			if ($objElement -> hasAttribute('framework', 'array')) {

				$arrContextValue = explode(',', $objElement -> getAttribute('framework', 'array') -> getValue());
				$this -> objContext -> setObject($sContextName, $arrContextValue);
				return;
			}


			// none of the previous types matched
			throw new ConfigurationException("Unknown context type.");
		}
	}